package au.org.arcs.dss.provider.datasource.provider.irods;

import java.util.*;
import java.io.*;

import edu.sdsc.grid.io.irods.*;
import edu.sdsc.grid.io.*;

import au.org.arcs.dss.provider.ConfigManager;
import au.org.arcs.dss.provider.datasource.provider.ProviderAccount;
import au.org.arcs.dss.provider.util.FileLock;
import au.org.arcs.dss.provider.util.SystemUtils;


/**
 *
 * @author ming
 */
public class IRODSService {
  private ProviderAccount providerAccount;
  private IRODSFileSystem irodsFileSystem;
  private static IRODSService irodsService;
  private static Map<String, FileLock> fileLocks = new HashMap<String, FileLock>();

  public static IRODSService getInstance(ProviderAccount providerAccount) {
    if (irodsService == null) {
      irodsService = new IRODSService(providerAccount);
    }

    return irodsService;
  }

  public IRODSService(ProviderAccount providerAccount) {
    this.providerAccount = providerAccount;
    this.irodsFileSystem = getIRODSFileSystem();
  }

  public static String getIrodsPath(String path, String name) {
    if (name != null && !name.isEmpty()) {
      return path + IRODSFile.PATH_SEPARATOR + name;
    } else {
      return path;
    }
  }

  public void close() {
    if (this.irodsFileSystem != null) {
      try {
        this.irodsFileSystem.close();
      } catch (Exception ex) {}
    }
  }

  public IRODSFile getIRODSFile(String path, boolean withDecendants) {
    IRODSFile irodsFile =  new IRODSFile(irodsFileSystem, path);

    ConfigManager.logger.debug(ConfigManager.key + ": Path: " + path);
    ConfigManager.logger.debug(ConfigManager.key + ": IRODS File: " + irodsFile.getAbsolutePath());

    if (withDecendants) {
      if (irodsFile.listFiles() != null && irodsFile.listFiles().length > 0) {
        irodsFile.listFiles()[0].getName();
      }
    }

    return irodsFile;
  }

  private IRODSFileSystem getIRODSFileSystem() {
    IRODSAccount account = createEncryptAccount(providerAccount.getValue(IRODSAccountMetaData.IRODS_HOST), providerAccount.getValue(IRODSAccountMetaData.IRODS_PORT),  providerAccount.getValue(IRODSAccountMetaData.IRODS_ZONE),  providerAccount.getValue(IRODSAccountMetaData.IRODS_DEFAULT_RESOURCE),  providerAccount.getValue(IRODSAccountMetaData.IRODS_PREFIX),  providerAccount.getValue(IRODSAccountMetaData.IRODS_USERNAME),  providerAccount.getValue(IRODSAccountMetaData.IRODS_PASSWORD));

    try {
      irodsFileSystem = new IRODSFileSystem(account);
    } catch (Exception ex) {
      ConfigManager.logger.error(ConfigManager.key + ": Error connecting IRODS File System.", ex);
    }

    return irodsFileSystem;
  }

  private static IRODSAccount createEncryptAccount(String host, String port,String zone, String defaultStorageResource, String homeDir, String username, String password) {
    IRODSAccount irods = null;

    ConfigManager.logger.debug(ConfigManager.key + ": Username " + username);

    try {
      if (homeDir != null &&  defaultStorageResource != null && zone != null) {
        irods = new IRODSAccount(host, Integer.parseInt(port), username, password, homeDir, zone, defaultStorageResource);
      }
    } catch (Exception ex) {
      ConfigManager.logger.error(ConfigManager.key + ": Error creating IRODS Account.", ex);
    }

    return irods;
  }

  public static File getBytesFromIRODSFile(IRODSFile irodsFile) {
    System.gc();

    if (!irodsFile.canRead()) {
      ConfigManager.logger.error(ConfigManager.key + ": Error reading data, Account doesn't have read permission.");
      return null;
    }

    IRODSFileInputStream irodsFileInputStream = null;
    int batchSize = ConfigManager.getConfigManagerInstance().getBatchSize();
    FileOutputStream output = null;
    File file = null;
    int totalFileLength = (int) irodsFile.length();

    // Read in the bytes
    int totalReadBytes = 0;
    int numRead = 0;
    boolean loadNewFile = false;
    String fileLockKey = irodsFile.getAbsolutePath();
    FileLock fileLock = fileLocks.get(fileLockKey);

    // We need to test whether we are blocking not same file.
    // Allowing different files to be accessed at the same time.
    if (fileLock == null) {
      // As no one is accessing the file, setup a lock on it and I will access it.
      synchronized (IRODSService.class) {
        fileLock = new FileLock();
        fileLock.setLocked(true);
        fileLocks.put(fileLockKey, fileLock);
      }
    } else if (fileLock.isLocked() != true) {
      synchronized (IRODSService.class) {
        fileLock.setLocked(true);
      }
    }

    synchronized(fileLock) {
      file = SystemUtils.getLoadedFile(irodsFile.getName(), new Date(irodsFile.lastModified()));

      if (file.length() == 0) {
        loadNewFile = true;
      }

      if (loadNewFile) {
        try {

            byte[] bytes = new byte[batchSize];
            output = new FileOutputStream(file);

            irodsFileInputStream = new IRODSFileInputStream(irodsFile);

            while (totalReadBytes < totalFileLength) {
              if (totalFileLength - totalReadBytes < batchSize) {
                batchSize = totalFileLength - totalReadBytes;
              }

              numRead = irodsFileInputStream.read(bytes, 0, batchSize);

              if (numRead < 0) {
                break;
              }

              output.write(bytes, 0, batchSize);

              totalReadBytes += numRead;
            }

            // Ensure all the bytes have been read in
            if (totalReadBytes < totalFileLength) {
              bytes = null;
              throw new IOException("Could not completely read file " + irodsFile.getName());
            }

            bytes = null;
        } catch (Exception ex) {
          ConfigManager.logger.error(ConfigManager.key + ": Error reading IRODS File Content.", ex);
        } finally {

          // Close the input stream and return bytes
          if (irodsFileInputStream != null) {
            try {
              irodsFileInputStream.close();

            } catch(Exception ex) {
              ConfigManager.logger.error(ConfigManager.key + ": Error closing IRODS File Input Stream.", ex);
            }
          }

          if (output != null) {
            try {
              output.close();
            } catch (Exception ex) {
              ConfigManager.logger.error(ConfigManager.key + ": Error closing File Input Stream.", ex);
            }
          }
        }
      }


      fileLock.setLocked(false);
    }

    // Change file last modified date to be the same as IRODS file last modified date.
    file.setLastModified(irodsFile.lastModified());

    return file;
 }

 public MetaDataRecordList[] listSubDirectories(IRODSFile parent) {
    MetaDataSelect selects[] = new MetaDataSelect[3];

    selects[0] = MetaDataSet.newSelection(IRODSMetaDataSet.DIRECTORY_TYPE);
    selects[1] = MetaDataSet.newSelection(GeneralMetaData.DIRECTORY_NAME);
    //selects[2] = MetaDataSet.newSelection(DirectoryMetaData.);
    selects[2] =  MetaDataSet.newSelection(DirectoryMetaData.DIRECTORY_MODIFY_DATE);

		MetaDataRecordList[] rl2;
		String path = "";

		MetaDataCondition con[] = new MetaDataCondition[1];

		try {
			// Have to do two queries, one for files and one for directories.
  		path = parent.getAbsolutePath();

      // get all the sub-directories
      con[0] = MetaDataSet.newCondition(
					IRODSMetaDataSet.PARENT_DIRECTORY_NAME,
					MetaDataCondition.EQUAL, path);
			rl2 = irodsFileSystem.query(con, selects);
 		  rl2 = MetaDataRecordList.getAllResults(rl2);

			// change to relative path
			if (rl2 != null) {
				String absolutePath = null;
				String relativePath = null;
				for (int i = 0; i < rl2.length; i++) {
					// only one record per rl
					absolutePath = rl2[i].getStringValue(1);
					relativePath = absolutePath.substring(absolutePath
							.lastIndexOf("/") + 1);
					rl2[i].setValue(0, relativePath);
				}
			}
		} catch (IOException e) {
			ConfigManager.logger.error("io exception is logged and ignored", e);
			return null;
		}

  	return rl2;
 }

 public MetaDataRecordList[] listSubFiles(IRODSFile parent) {
	  Map<String, MetaDataRecordList> recordMap = new HashMap<String, MetaDataRecordList>();
    MetaDataSelect selects[] = new MetaDataSelect[4];

    selects[0] = MetaDataSet.newSelection(GeneralMetaData.FILE_NAME);
    selects[1] = MetaDataSet.newSelection(GeneralMetaData.DIRECTORY_NAME);
    selects[2] = MetaDataSet.newSelection(IRODSMetaDataSet.SIZE);
	  //selects[3] = MetaDataSet.newSelection(GeneralMetaData.MODIFICATION_DATE);
    selects[3] = MetaDataSet.newSelection(IRODSMetaDataSet.MODIFICATION_DATE);

		MetaDataRecordList[] rl1;
		String path = "";

		MetaDataCondition con[] = new MetaDataCondition[1];

		try {
			// Have to do two queries, one for files and one for directories.
  		path = parent.getAbsolutePath();

			// Get all the files
     	con[0] = MetaDataSet.newCondition(GeneralMetaData.DIRECTORY_NAME,
					MetaDataCondition.EQUAL, path);
			rl1 = irodsFileSystem.query(con, selects);
			rl1 = MetaDataRecordList.getAllResults(rl1);
  	} catch (IOException e) {
			ConfigManager.logger.error("IO exception is logged and ignored", e);
			return null;
		}

    if (rl1 != null) {
      for (MetaDataRecordList recordList : rl1) {
        if (!recordMap.containsKey(recordList.getStringValue(0))) {
          recordMap.put(recordList.getStringValue(0), recordList);
        }
      }
    }

    return (MetaDataRecordList[]) recordMap.values().toArray(new MetaDataRecordList[recordMap.size()]);
	}
}